﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NAudio;
using NAudio.Wave;
using NAudio.Utils;

namespace voiceChanger
{
    public class LVoice
    {
        private static int num = slCommunicator.Properties.Settings.Default.voiceChanger_mixCount;
        private static float lowestPitch = slCommunicator.Properties.Settings.Default.voiceChanger_minPitch;
        private static float pitchIncrement = slCommunicator.Properties.Settings.Default.voiceChanger_pitchIncrement;

        private BufferedWaveProvider[] recorders = new BufferedWaveProvider[num];
        private EffectStream[] estreams = new EffectStream[num];
        private SuperPitch[] epitch = new SuperPitch[num];

        private MixingSampleProvider mixersp;
        private IWaveProvider outputwave;

        public bool bEnabled { get; set; }
        public event EventHandler<WaveInEventArgs> OutputAvailable;

        public LVoice(WaveFormat wf)
        {
            bEnabled = slCommunicator.Properties.Settings.Default.voiceChanger_bEnable;
            for (int i = 0; i < num; i++)
            {
                recorders[i] = new BufferedWaveProvider(wf);
                recorders[i].DiscardOnBufferOverflow = true;
                estreams[i] = new EffectStream(recorders[i].ToSampleProvider());

                epitch[i] = new SuperPitch();
                epitch[i].Sliders[1].Value = (lowestPitch + (i * pitchIncrement));
                estreams[i].UpdateEffectChain(new Effect[] { epitch[i] });
            }

            mixersp = new MixingSampleProvider(estreams);
            outputwave = new WaveFloatTo16Provider(new StoWProvider(mixersp));
        }

        public ISampleProvider OutputSampleProvider
        {
            get { return mixersp; }
        }
        public IWaveProvider OutputWaveProvider
        {
            get { return outputwave; }
        }

        public void DataAvailable(object sender, WaveInEventArgs e)
        {
            if (OutputAvailable == null)
                return;

            if (bEnabled)
            {
                foreach (BufferedWaveProvider bwp in recorders)
                    bwp.AddSamples(e.Buffer, 0, e.BytesRecorded);

                byte[] d = new byte[e.BytesRecorded];
                int red = OutputWaveProvider.Read(d, 0, d.Length);

                OutputAvailable(sender, new WaveInEventArgs(d, red));
            }
            else
                OutputAvailable(sender, e);
        }
    }

    public class StoWProvider : IWaveProvider
    {
        ISampleProvider source;
        public StoWProvider(ISampleProvider isp)
        {
            source = isp;
        }

        public WaveFormat WaveFormat
        {
            get { return source.WaveFormat; }
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            float[] fb = new float[count/4];
            int lenf = source.Read(fb, 0, fb.Length);

            Buffer.BlockCopy(fb, 0, buffer, offset, count);

            return lenf * 4;
        }
    }

    public class Slider
    {
        List<string> discreteValueText;

        public Slider(float defaultValue, float minimum, float maximum, float increment, string description)
        {
            this.Default = defaultValue;
            this.Value = defaultValue;
            this.Minimum = minimum;
            this.Maximum = maximum;
            this.Increment = increment;
            this.Description = description;
            this.discreteValueText = new List<string>();
        }

        public string Description { get; set; }
        public float Default { get; private set; }
        public float Minimum { get; private set; }
        public float Maximum { get; private set; }
        public float Increment { get; private set; }
        public float Value { get; set; }

        public IList<string> DiscreteValueText { get { return discreteValueText; } }
    }

    public abstract class Effect
    {
        private List<Slider> sliders;
        public float SampleRate { get; set; }
        public float Tempo { get; set; }
        public bool Enabled { get; set; }

        public Effect()
        {
            sliders = new List<Slider>();
            Enabled = true;
            Tempo = 120;
            SampleRate = 44100;
        }

        public IList<Slider> Sliders { get { return sliders; } }

        public Slider AddSlider(float defaultValue, float minimum, float maximum, float increment, string description)
        {
            Slider slider = new Slider(defaultValue, minimum, maximum, increment, description);
            sliders.Add(slider);
            return slider;
        }

        public abstract string Name { get; }

        // helper base methods
        // these are primarily to enable derived classes to use a similar
        // syntax to JS effects
        protected float slider1 { get { return sliders[0].Value; } }
        protected float slider2 { get { return sliders[1].Value; } }
        protected float slider3 { get { return sliders[2].Value; } }
        protected float slider4 { get { return sliders[3].Value; } }
        protected float slider5 { get { return sliders[4].Value; } }
        protected float slider6 { get { return sliders[5].Value; } }
        protected float slider7 { get { return sliders[6].Value; } }
        protected float slider8 { get { return sliders[7].Value; } }
        protected float min(float a, float b) { return Math.Min(a, b); }
        protected float max(float a, float b) { return Math.Max(a, b); }
        protected float abs(float a) { return Math.Abs(a); }
        protected float exp(float a) { return (float)Math.Exp(a); }
        protected float sqrt(float a) { return (float)Math.Sqrt(a); }
        protected float sin(float a) { return (float)Math.Sin(a); }
        protected float tan(float a) { return (float)Math.Tan(a); }
        protected float cos(float a) { return (float)Math.Cos(a); }
        protected float pow(float a, float b) { return (float)Math.Pow(a, b); }
        protected float sign(float a) { return Math.Sign(a); }
        protected float log(float a) { return (float)Math.Log(a); }
        protected float PI { get { return (float)Math.PI; } }

        protected void convolve_c(float[] buffer1, int offset1, float[] buffer2, int offset2, int count)
        {
            for (int i = 0; i < count * 2; i += 2)
            {
                float r = buffer1[offset1 + i];
                float im = buffer1[offset1 + i + 1];
                float cr = buffer2[offset2 + i];
                float ci = buffer2[offset2 + i + 1];
                buffer1[offset1 + i] = r * cr - im * ci;
                buffer1[offset1 + i + 1] = r * ci + im * cr;
            }
        }

        /// <summary>
        /// Should be called on effect load, sample rate changes, and start of playback
        /// </summary>
        public virtual void Init()
        {
        }

        private volatile bool sliderChanged;

        public void SliderChanged()
        {
            sliderChanged = true;
        }

        /// <summary>
        /// will be called when a slider value has been changed
        /// </summary>
        protected abstract void Slider();

        /// <summary>
        /// called before each block is processed
        /// </summary>
        /// <param name="samplesblock">number of samples in this block</param>
        public virtual void Block(int samplesblock)
        {
        }

        public void OnSample(ref float left, ref float right)
        {
            if (sliderChanged)
            {
                Slider();
                sliderChanged = false;
            }
            Sample(ref left, ref right);
        }

        /// <summary>
        /// called for each sample
        /// </summary>        
        protected abstract void Sample(ref float spl0, ref float spl1);

        public override string ToString()
        {
            return Name;
        }
    }

    public class EffectChain : IEnumerable<Effect>
    {
        private readonly List<Effect> effects;
        public event EventHandler<EventArgs> Modified;

        protected virtual void OnModified()
        {
            EventHandler<EventArgs> handler = Modified;
            if (handler != null) handler(this, EventArgs.Empty);
        }

        public EffectChain()
        {
            effects = new List<Effect>();
        }

        public void Add(Effect effect)
        {
            effects.Add(effect);
            OnModified();
        }

        public bool MoveUp(Effect effect)
        {
            int index = effects.IndexOf(effect);
            if (index == -1)
            {
                throw new ArgumentException("The specified effect is not part of this effect chain");
            }
            if (index > 0)
            {
                effects.RemoveAt(index);
                effects.Insert(index - 1, effect);
                OnModified();
                return true;
            }
            return false;
        }

        public bool MoveDown(Effect effect)
        {
            int index = effects.IndexOf(effect);
            if (index == -1)
            {
                throw new ArgumentException("The specified effect is not part of this effect chain");
            }
            if (index < Count - 1)
            {
                effects.RemoveAt(index);
                effects.Insert(index + 1, effect);
                OnModified();
                return true;
            }
            return false;
        }

        public void Clear()
        {
            effects.Clear();
            OnModified();
        }

        public bool Contains(Effect item)
        {
            return effects.Contains(item);
        }

        public int Count
        {
            get { return effects.Count; }
        }

        public bool Remove(Effect effect)
        {
            var removed = effects.Remove(effect);
            if (removed)
                OnModified();
            return removed;
        }

        public IEnumerator<Effect> GetEnumerator()
        {
            return effects.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return effects.GetEnumerator();
        }
    }

    public class EffectStream : ISampleProvider
    {
        private readonly List<Effect> effects;
        private readonly ISampleProvider sourceProvider;
        private readonly object effectLock = new object();

        public EffectStream(ISampleProvider sourceProvider)
        {
            this.effects = new List<Effect>();
            this.sourceProvider = sourceProvider;
            foreach (var effect in effects)
            {
                InitialiseEffect(effect);
            }
        }

        public WaveFormat WaveFormat
        {
            get { return sourceProvider.WaveFormat; }
        }

        public int Read(float[] buffer, int offset, int count)
        {
            int read = sourceProvider.Read(buffer, offset, count);

            lock (effectLock)
            {
                Process(buffer, offset, read);
            }
            return read;
        }

        private void Process(float[] buffer, int offset, int count)
        {
            int samples = count;
            foreach (var effect in effects.Where(e => e.Enabled))
            {
                effect.Block(samples);
            }

            for (int sample = 0; sample < samples; sample++)
            {
                float sampleLeft = buffer[offset];
                float sampleRight = sampleLeft;
                if (WaveFormat.Channels == 2)
                {
                    sampleRight = buffer[offset + 1];
                    sample++;
                }

                // run these samples through the effect
                foreach (var effect in effects.Where(e => e.Enabled))
                {
                    effect.OnSample(ref sampleLeft, ref sampleRight);
                }

                // put them back
                buffer[offset++] = sampleLeft;
                if (WaveFormat.Channels == 2)
                {
                    buffer[offset++] = sampleRight;
                }
            }
        }


        public void UpdateEffectChain(Effect[] newEffects)
        {
            lock (effectLock)
            {
                foreach (var newEffect in newEffects.Except(effects))
                {
                    InitialiseEffect(newEffect);
                }
                effects.Clear();
                effects.AddRange(newEffects);
            }
        }

        private void InitialiseEffect(Effect effect)
        {
            effect.SampleRate = WaveFormat.SampleRate;
            effect.Init();
            effect.SliderChanged();
        }

        public bool RemoveEffect(Effect effect)
        {
            lock (effectLock)
            {
                return this.effects.Remove(effect);
            }
        }
    }

    public class SuperPitch : Effect
    {
        int bufsize;
        float xfade;
        int bufloc0;
        int bufloc1;
        int buffer0;
        int buffer1;
        int bufdiff;
        float pitch;

        float denorm;
        bool filter;
        float v0;
        float h01, h02, h03, h04;
        float h11, h12, h13, h14;
        float a1, a2, a3, b1, b2;
        float t0, t1;
        float drymix;
        float wetmix;
        float[] buffer = new float[64000];

        public SuperPitch()
        {
            AddSlider(0, -100, 100, 1, "Pitch adjust (cents)");
            AddSlider(5, -12, 12, 1, "Pitch adjust (semitones)");
            AddSlider(0, -8, 8, 1, "Pitch adjust (octaves)"); // mrh 12 octaves up causes issues, reigning this in a bit
            AddSlider(50, 1, 200, 1, "Window size (ms)"); // mrh minimum window size set to 1 as 0 seems to cause issues
            AddSlider(20, 0.05f, 50, 0.5f, "Overlap size (ms)");
            AddSlider(0, -120, 6, 1, "Wet mix (dB)");
            AddSlider(-120, -120, 6, 1, "Dry mix (dB)");
            Slider filterSlider = AddSlider(1, 0, 1, 1, "Filter"); // {NO,YES}filter
            filterSlider.DiscreteValueText.Add("Off");
            filterSlider.DiscreteValueText.Add("On");
        }

        public override string Name
        {
            get { return "SuperPitch"; }
        }

        public override void Init()
        {
            bufsize = (int)SampleRate; // srate|0;
            xfade = 100;
            bufloc0 = 10000;
            bufloc1 = bufloc0 + bufsize + 1000;

            buffer0 = bufloc0;
            buffer1 = bufloc1;
            bufdiff = bufloc1 - bufloc0;
            pitch = 1.0f;
            denorm = pow(10, -20);
            base.Init();
        }

        protected override void Slider()
        {
            filter = slider8 > 0.5;
            int bsnew = (int)(Math.Min(slider4, 1000) * 0.001 * SampleRate);
            //   bsnew=(min(slider4,1000)*0.001*srate)|0;
            if (bsnew != bufsize)
            {
                bufsize = bsnew;
                v0 = buffer0 + bufsize * 0.5f;
                if (v0 > bufloc0 + bufsize)
                {
                    v0 -= bufsize;
                }
            }

            xfade = (int)(slider5 * 0.001 * SampleRate);
            if (xfade > bsnew * 0.5)
            {
                xfade = bsnew * 0.5f;
            }

            float npitch = pow(2, ((slider2 + slider1 * 0.01f) / 12 + slider3));
            if (pitch != npitch)
            {
                pitch = npitch;
                float lppos = (pitch > 1.0f) ? 1.0f / pitch : pitch;
                if (lppos < (0.1f / SampleRate))
                {
                    lppos = 0.1f / SampleRate;
                }
                float r = 1.0f;
                float c = 1.0f / tan(PI * lppos * 0.5f);
                a1 = 1.0f / (1.0f + r * c + c * c);
                a2 = 2 * a1;
                a3 = a1;
                b1 = 2.0f * (1.0f - c * c) * a1;
                b2 = (1.0f - r * c + c * c) * a1;
                h01 = h02 = h03 = h04 = 0;
                h11 = h12 = h13 = h14 = 0;
            }

            drymix = pow(2, (slider7 / 6));
            wetmix = pow(2, (slider6 / 6));
        }

        protected override void Sample(ref float spl0, ref float spl1)
        {
            int iv0 = (int)(v0);
            float frac0 = v0 - iv0;
            int iv02 = (iv0 >= (bufloc0 + bufsize - 1)) ? iv0 - bufsize + 1 : iv0 + 1;

            float ren0 = (buffer[iv0 + 0] * (1 - frac0) + buffer[iv02 + 0] * frac0);
            float ren1 = (buffer[iv0 + bufdiff] * (1 - frac0) + buffer[iv02 + bufdiff] * frac0);
            float vr = pitch;
            float tv, frac, tmp, tmp2;
            if (vr >= 1.0)
            {
                tv = v0;
                if (tv > buffer0) tv -= bufsize;
                if (tv >= buffer0 - xfade && tv < buffer0)
                {
                    // xfade
                    frac = (buffer0 - tv) / xfade;
                    tmp = v0 + xfade;
                    if (tmp >= bufloc0 + bufsize) tmp -= bufsize;
                    tmp2 = (tmp >= bufloc0 + bufsize - 1) ? bufloc0 : tmp + 1;
                    ren0 = ren0 * frac + (1 - frac) * (buffer[(int)tmp + 0] * (1 - frac0) + buffer[(int)tmp2 + 0] * frac0);
                    ren1 = ren1 * frac + (1 - frac) * (buffer[(int)tmp + bufdiff] * (1 - frac0) + buffer[(int)tmp2 + bufdiff] * frac0);
                    if (tv + vr > buffer0 + 1) v0 += xfade;
                }
            }
            else
            {// read pointer moving slower than write pointer
                tv = v0;
                if (tv < buffer0) tv += bufsize;
                if (tv >= buffer0 && tv < buffer0 + xfade)
                {
                    // xfade
                    frac = (tv - buffer0) / xfade;
                    tmp = v0 + xfade;
                    if (tmp >= bufloc0 + bufsize) tmp -= bufsize;
                    tmp2 = (tmp >= bufloc0 + bufsize - 1) ? bufloc0 : tmp + 1;
                    ren0 = ren0 * frac + (1 - frac) * (buffer[(int)tmp + 0] * (1 - frac0) + buffer[(int)tmp2 + 0] * frac0);
                    ren1 = ren1 * frac + (1 - frac) * (buffer[(int)tmp + bufdiff] * (1 - frac0) + buffer[(int)tmp2 + bufdiff] * frac0);
                    if (tv + vr < buffer0 + 1) v0 += xfade;
                }
            }


            if ((v0 += vr) >= (bufloc0 + bufsize)) v0 -= bufsize;

            float os0 = spl0;
            float os1 = spl1;
            if (filter && pitch > 1.0)
            {

                t0 = spl0; t1 = spl1;
                spl0 = a1 * spl0 + a2 * h01 + a3 * h02 - b1 * h03 - b2 * h04 + denorm;
                spl1 = a1 * spl1 + a2 * h11 + a3 * h12 - b1 * h13 - b2 * h14 + denorm;
                h02 = h01; h01 = t0;
                h12 = h11; h11 = t1;
                h04 = h03; h03 = spl0;
                h14 = h13; h13 = spl1;
            }


            buffer[buffer0 + 0] = spl0; // write after reading it to avoid clicks
            buffer[buffer0 + bufdiff] = spl1;

            spl0 = ren0 * wetmix;
            spl1 = ren1 * wetmix;

            if (filter && pitch < 1.0)
            {
                t0 = spl0; t1 = spl1;
                spl0 = a1 * spl0 + a2 * h01 + a3 * h02 - b1 * h03 - b2 * h04 + denorm;
                spl1 = a1 * spl1 + a2 * h11 + a3 * h12 - b1 * h13 - b2 * h14 + denorm;
                h02 = h01; h01 = t0;
                h12 = h11; h11 = t1;
                h04 = h03; h03 = spl0;
                h14 = h13; h13 = spl1;
            }

            spl0 += os0 * drymix;
            spl1 += os1 * drymix;

            if ((buffer0 += 1) >= (bufloc0 + bufsize)) buffer0 -= bufsize;

        }
    }

    public class MixingSampleProvider : ISampleProvider
    {
        private List<ISampleProvider> sources;
        private WaveFormat waveFormat;
        private float[] sourceBuffer;
        private const int maxInputs = 1024; // protect ourselves against doing something silly

        /// <summary>
        /// Creates a new MixingSampleProvider, with no inputs, but a specified WaveFormat
        /// </summary>
        /// <param name="waveFormat">The WaveFormat of this mixer. All inputs must be in this format</param>
        public MixingSampleProvider(WaveFormat waveFormat)
        {
            this.sources = new List<ISampleProvider>();
            this.waveFormat = waveFormat;
        }

        /// <summary>
        /// Creates a new MixingSampleProvider, based on the given inputs
        /// </summary>
        /// <param name="sources">Mixer inputs - must all have the same waveformat, and must
        /// all be of the same WaveFormat</param>
        public MixingSampleProvider(IEnumerable<ISampleProvider> sources)
        {
            this.sources = new List<ISampleProvider>();
            foreach (var source in sources)
            {
                AddMixerInput(source);
            }
        }

        /// <summary>
        /// Adds a new mixer input
        /// </summary>
        /// <param name="mixerInput">Mixer input</param>
        public void AddMixerInput(ISampleProvider mixerInput)
        {
            // we'll just call the lock around add since we are protecting against an AddMixerInput at
            // the same time as a Read, rather than two AddMixerInput calls at the same time
            lock (sources)
            {
                if (this.sources.Count >= maxInputs)
                {
                    throw new InvalidOperationException("Too many mixer inputs");
                }
                this.sources.Add(mixerInput);
            }
            if (this.waveFormat == null)
            {
                this.waveFormat = mixerInput.WaveFormat;
            }
            else
            {
                if (this.WaveFormat.SampleRate != mixerInput.WaveFormat.SampleRate ||
                    this.WaveFormat.Channels != mixerInput.WaveFormat.Channels)
                {
                    throw new ArgumentException("All mixer inputs must have the same WaveFormat");
                }
            }
        }

        /// <summary>
        /// Removes a mixer input
        /// </summary>
        /// <param name="mixerInput">Mixer input to remove</param>
        public void RemoveMixerInput(ISampleProvider mixerInput)
        {
            lock (sources)
            {
                this.sources.Remove(mixerInput);
            }
        }

        /// <summary>
        /// Removes all mixer inputs
        /// </summary>
        public void RemoveAllMixerInputs()
        {
            lock (sources)
            {
                this.sources.Clear();
            }
        }

        /// <summary>
        /// The output WaveFormat of this sample provider
        /// </summary>
        public WaveFormat WaveFormat
        {
            get { return this.waveFormat; }
        }

        /// <summary>
        /// Reads samples from this sample provider
        /// </summary>
        /// <param name="buffer">Sample buffer</param>
        /// <param name="offset">Offset into sample buffer</param>
        /// <param name="count">Number of samples required</param>
        /// <returns>Number of samples read</returns>
        public int Read(float[] buffer, int offset, int count)
        {
            int outputSamples = 0;
            this.sourceBuffer = BufferHelpers.Ensure(this.sourceBuffer, count);
            lock (sources)
            {
                int index = sources.Count - 1;
                while (index >= 0)
                {
                    var source = sources[index];
                    int samplesRead = source.Read(this.sourceBuffer, 0, count);
                    int outIndex = offset;
                    for (int n = 0; n < samplesRead; n++)
                    {
                        if (n >= outputSamples)
                        {
                            buffer[outIndex++] = this.sourceBuffer[n];
                        }
                        else
                        {
                            buffer[outIndex++] += this.sourceBuffer[n];
                        }
                    }
                    outputSamples = Math.Max(samplesRead, outputSamples);
                    if (samplesRead == 0)
                    {
                        sources.RemoveAt(index);
                    }
                    index--;
                }
            }
            return outputSamples;
        }
    }
}
